/*global Float32Array */
var NOOGL = (function () {
    'use strict';

    Array.prototype.flatten = Array.prototype.flatten || function () {
        var rv = [], i;
        for (i = 0; i < this.length; i += 1) {
            if (this[i] instanceof Array) {
                rv = rv.concat(this[i].flatten());
            } else {
                rv = rv.concat(this[i]);
            }
        }
        return rv;
    };

    function VertexArrayBuffer(context, size) {
        this.context = context;
        this.size = size;

        this.vbo = context.createBuffer();
    }

    VertexArrayBuffer.prototype.bind = function () {
        this.context.bindBuffer(this.context.ARRAY_BUFFER, this.vbo);
    };

    VertexArrayBuffer.prototype.setData = function (data) {
        this.N = data.length / this.size;

        this.bind();
        this.context.bufferData(this.context.ARRAY_BUFFER, data, this.context.STREAM_DRAW);
    };

    VertexArrayBuffer.prototype.enableAttribute = function (attrib) {
        this.bind();
        this.context.enableVertexAttribArray(attrib);
        this.context.vertexAttribPointer(attrib, this.size, this.context.FLOAT, false, 0, 0);
    };

    VertexArrayBuffer.prototype.drawTriangles = function () {
        this.bind();
        this.context.drawArrays(this.context.TRIANGLES, 0, this.N);
    };

    function TriangleMesh(program, vertices) {
        this.vaoPosition = program.createVAB('position', vertices, 3);
    }

    TriangleMesh.prototype.render = function (canvas) {
        this.vaoPosition.drawTriangles();
    };

    function Shader(context, type, source) {
        function shaderType(type) {
            if (type === 'x-glsl/x-fragment') {
                return context.FRAGMENT_SHADER;
            }
            if (type === 'x-glsl/x-vertex') {
                return context.VERTEX_SHADER;
            }
            throw new Error(JSON.stringify(type) + ' is not a valid shader type');
        }

        var shader = context.createShader(shaderType(type));
        context.shaderSource(shader, source);
        context.compileShader(shader);

        if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
            throw new Error(context.getShaderInfoLog(shader));
        }

        this.context = context;
        this.shader = shader;
    }

    Shader.prototype.type = function () {
        return this.context.getShaderParameter(this.shader, this.context.SHADER_TYPE);
    };

    Shader.prototype.attachTo = function (program) {
        this.context.attachShader(program, this.shader);
    };


    function Program(context, shaders) {
        var program = context.createProgram(), i;
        for (i = 0; i < shaders.length; i += 1) {
            if (shaders[i] instanceof Shader) {
                shaders[i].attachTo(program);
            } else {
                throw new Error('argument not a shader');
            }
        }
        context.linkProgram(program);

        if (!context.getProgramParameter(program, context.LINK_STATUS)) {
            throw new Error(context.getProgramInfoLog(program));
        }

        this.context = context;
        this.program = program;
    }

    Program.prototype.getAttributes = function () {
        var context = this.context, program = this.program, n = context.getProgramParameter(program, context.ACTIVE_ATTRIBUTES), list = [], i;
        for (i = 0; i < n; i += 1) {
            list.push(context.getActiveAttrib(program, i).name);
        }
        return list;
    };

    Program.prototype.getUniforms = function () {
        var context = this.context, program = this.program, n = context.getProgramParameter(program, context.ACTIVE_UNIFORMS), list = [], i;
        for (i = 0; i < n; i += 1) {
            list.push(context.getActiveUniform(program, i).name);
        }
        return list;
    };

    Program.prototype.getAttribLocation = function (attrib) {
        return this.context.getAttribLocation(this.program, attrib);
    };

    Program.prototype.use = function () {
        this.context.useProgram(this.program);
    };

    Program.prototype.createVAB = function (attrib, data, size) {
        var vab = new VertexArrayBuffer(this.context, size);

        vab.setData(new Float32Array(data.flatten()));
        vab.enableAttribute(this.getAttribLocation(attrib));

        return vab;
    };

    function Scene() {
        this.objects = [];
    }

    Scene.prototype.render = function (gl) {
        var i;
        gl.clear();

        for (i = 0; i < this.objects.length; i += 1) {
            this.objects[i].render(gl);
        }
    };
    Scene.prototype.add = function () {
        this.objects = this.objects.concat(Array.prototype.slice.apply(arguments));
    };

    function Canvas(options) {
        this.canvas = document.createElement('canvas');

        if (options && options.width) {
            this.canvas.width = options.width;
        }
        if (options && options.height) {
            this.canvas.height = options.height;
        }

        // needs to be experimental for Ie and makes Firefox behave better
        this.context = this.canvas.getContext('experimental-webgl');
        this.context.clearColor(0, 0, 0, 1);
    }

    Canvas.prototype.createProgram = function () {
        return new Program(this.context, Array.prototype.slice.call(arguments));
    };

    Canvas.prototype.createShaderFromSource = function (type, source) {
        return new Shader(this.context, type, source);
    };

    Canvas.prototype.createShaderFromScript = function (script) {
        if (!script) {
            throw new Error('invalid argument');
        }

        return this.createShaderFromSource(script.type, script.textContent);
    };

    Canvas.prototype.appendTo = function (parent) {
        parent.appendChild(this.canvas);
    };

    Canvas.prototype.clear = function () {
        this.context.clear(this.context.COLOR_BUFFER_BIT);
    };


    return {
        Canvas: Canvas,
        Scene: Scene,
        TriangleMesh: TriangleMesh
    };
}());
